#
# This file is part of Contentbus

# Contentbus is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.

# Contentbus is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.

# You should have received a copy of the GNU General Public License
# along with Contentbus.  If not, see <http://www.gnu.org/licenses/>.

"""Object-matching related classes"""

from functools import partial
from inspect import getargspec


OP_SPLIT_TOKEN = '__'


class UnknownOperator(Exception):
    """Raised when an unknow operator is found."""


class OperatorAlreadyRegistered(Exception):
    """Raised when an operator with same name is already registered."""


def negate(function):
    """
    Return a negated version of a funcion.

    @param function: a boolean function to negate.
    @return: the negated function.
    """
    def negated(*args, **kwargs):
        return not function(*args, **kwargs)

    negated.func_name = function.func_name
    negated.func_doc = function.func_doc
    return negated


class MatchOperator(object):
    """
    Match operator on objects attributes.

    @param function: the matching function.

    the C{function} signature must be::

      foo(*params, attr_name=None, obj=None) -> bool

    where C{params} are values for C{attr_name}.
    """

    OPERATORS = {}

    def __init__(self, function):
        # check function signature
        spec = getargspec(function)
        params = frozenset(("attr_name", "obj"))
        if not params.issubset(spec.args) and spec.defaults != (None, None):
            raise ValueError("Function signature is not correct")

        self.function = function

    @staticmethod
    def register(operator, name, overwrite=False):
        """
        Register a L{MatchOperator}.

        @param operator: a L{MatchOperator} instance or a C{callable} to
          register.
        @param name: operator name, must not contain '__'.
        @param overwrite: if C{True}, the operator will overwrite a previously
          defined one.
        """

        if not isinstance(operator, MatchOperator):
            # we assume it's a callable, wrap it with a MatchOperator
            operator = MatchOperator(operator)

        if not name:
            raise ValueError("Can't register with empty name")

        if OP_SPLIT_TOKEN in name:
            raise ValueError("MatchOperator name can't contain '%s'",
                             OP_SPLIT_TOKEN)

        if name in MatchOperator.OPERATORS and not overwrite:
            raise OperatorAlreadyRegistered("MatchOperator already registered")

        MatchOperator.OPERATORS[name] = operator

    @staticmethod
    def unregister(name):
        """
        Unregister a L{MatchOperator}.

        @param name: the name used to register the L{MatchOperator}.
        """
        if name in MatchOperator.OPERATORS:
            del MatchOperator.OPERATORS[name]

    @staticmethod
    def get_operator(name):
        """
        Return operator by name.

        @param name: the name of the operator.
        @return the L{MatchOperator}.
        """
        try:
            return MatchOperator.OPERATORS[name]
        except KeyError:
            raise UnknownOperator("MatchOperator not registered: '%s'" % name)

    @staticmethod
    def operator(name):
        """
        Decorator to register a function as operator with specified name.

        @param name: the operator name.

        Operators registered via decorator overwrite any operator with same
        name.
        """
        def operatorizer(function):
            MatchOperator.register(function, name, overwrite=True)
            return function

        return operatorizer

    def __call__(self, attr_name, params, negated=False):
        """
        This returns a partial function, with attribute and parameters set.

        @param attr_name: the name of the attribute to match to C{params}.
        @param params: a list with parameters for attribute matching.
        @param negated: if True, the boolean result will be negated.
        @return: a function with the following signature::

          function(obj=None) -> bool

        which matches an object againts specified parameters
        """

        return partial(self.function if not negated else negate(self.function),
                       *params, attr_name=attr_name)


class ObjectMatcher(object):
    """
    A matcher for an object type.

    @param object_type: the type of the objects to match.
    @param match_fun: an optional function to match objects against.
    @param match_params: optional match parameters.
    """

    def __init__(self, object_type, match_fun=None, **match_params):
        self.__object_type = object_type
        self.__match_fun = match_fun
        self.__match_conditions = []
        negated = False

        for param, value in match_params.iteritems():
            if OP_SPLIT_TOKEN not in param:
                attr_name = param
                match_op_name = ""
            else:
                attr_name, match_op_name = param.rsplit(OP_SPLIT_TOKEN, 1)
                # handle inverse logic
                if match_op_name == "not" or match_op_name.startswith("not_"):
                    match_op_name = match_op_name[4:]
                    negated = True

            if not match_op_name:
                match_op_name = "eq"

            params = value if type(value) in (tuple, list) else (value,)

            self.__match_conditions.append(
                MatchOperator.get_operator(match_op_name)(
                    attr_name, params, negated))

    @property
    def object_type(self):
        return self.__object_type

    @property
    def match_function(self):
        return self.__match_fun

    def matches(self, obj):
        """
        Match against an object

        @param obj: the object to match against conditions.
        @return: true if object matches conditions.
        """
        if not isinstance(obj, self.__object_type):
            return False

        # if no condition is provided, always match for the object type
        if not self.__match_fun and not self.__match_conditions:
            return True

        if self.__match_fun:
            return self.__match_fun(obj)

        # run all condition matchers
        try:
            return all(match(obj=obj) for match in self.__match_conditions)
        except Exception:
            return False  # if any match raises an error, match is failed
